#
#  AppController.rb
#  RPub
#
#  Created by 서구 허 on 08. 03. 05.
#  Copyright (c) 2008 __MyCompanyName__. All rights reserved.
#

require 'osx/cocoa'
require 'blog'
require 'time'
require 'uri'

OSX.require_framework 'Webkit'

MARGIN = 10.0
INTERVAL = 10.0

# functions related to making a new button / textview and position them in a view
module Decorate
  def init_position(view)
    {:x=>MARGIN, :y=>view.bounds.height-MARGIN, :maxx=>view.bounds.width, :maxheight=>0, :total=>MARGIN*2 }
  end
  
  def new_line(pos)
    pos[:x] = MARGIN
    pos[:y] -= pos[:maxheight] + INTERVAL
    pos[:total] += pos[:maxheight] + INTERVAL
    pos[:maxheight] = 0
  end
  
  def position_control(control, pos)
    new_line(pos) if pos[:x]+control.bounds.width+MARGIN > pos[:maxx]
    control.setFrame([pos[:x], pos[:y]-control.bounds.height, control.bounds.width, control.bounds.height])
    pos[:x] += INTERVAL + control.bounds.width
    pos[:maxheight] = control.bounds.height if control.bounds.height > pos[:maxheight]
  end
  
  def new_button(view, title, pos)
    button = OSX::NSButton.alloc.init
    button.setTitle(title)
    button.setButtonType(OSX::NSSwitchButton)
    button.sizeToFit
    position_control(button, pos)
    view.addSubview(button)
    button
  end
  
  def new_text(view, title, default, pos)
    label = OSX::NSTextField.alloc.init
    label.setStringValue(title)
    label.sizeToFit
    label.setBezeled(false)
    label.setDrawsBackground(false)
    label.setEditable(false)
    label.setSelectable(false)
    position_control(label, pos)
    view.addSubview(label)
    
    field = OSX::NSTextField.alloc.init
    field.sizeToFit
    field.setFrame([0, 0, 200.0, field.bounds.height])
    field.setStringValue(default) unless default.nil?
    position_control(field, pos)
    view.addSubview(field)
    field
  end
  
  def make_taller(view, height)
    cview = view
    f = cview.frame
    while f.height < height
      diff = height - f.height
      if cview.class == OSX::NSBox then
        cview.setContentViewMargins([0.0, 0.0])
      end
      cview.setFrame([f.x, f.y-diff, f.width, height])
      if cview == view then                     # just reposition controls just added
        cview.subviews.each do |v|
          cf = v.frame
          v.setFrame([cf.x, cf.y+diff-MARGIN, cf.width, cf.height])
        end
      else                                      # reposition and resize controls needed
        last = cview.superview.subviews[-1]
        cview.superview.subviews.each do |v|
          next if v == cview
          cf = v.frame
          if v == last then
            v.setFrame([cf.x, cf.y, cf.width, cf.height-diff])
          else
            v.setFrame([cf.x, cf.y-diff, cf.width, cf.height])
          end
        end
      end
      cview = cview.superview
      f = cview.frame
    end
  end
end

class AppController < OSX::NSObject
  include Decorate
  ib_outlet :main_win, :box, :title, :content
  ib_outlet :preview, :webview, :article_list, :delete
  ib_action :new_article
  ib_action :view_article
  ib_action :post_article
  ib_action :save_article
  ib_action :delete_draft
  
  def initialize
    super
    @sites = Site.find(:all)
    @categories = {}              # holds count of categories
    @sclass_counter = {}          # count for number of site class
    @sclass_controls = {}         # remember site class related controls
    @cat_buttons = []
    @site_hash = {}               # dictionary of object_id => class_name_symbol to find out class of site button
    @site_btns = []               # dictionary of object_id => class_name_symbol to find out class of site button
    @article_id = nil
    @selected_article = nil       # to find out selected article id on outlineViewSelectionDidChange
    @article_changed = false
    @tag_field = nil
    @blog_data_source = nil
    $KCODE='U'
  end
  
  def decorate(view)
    # create & calculate each control's size
    cat_order = []
    # sites
    #pos = {:x=>MARGIN, :y=>view.bounds.height-MARGIN, :maxx=>view.bounds.width, :maxheight=>0, :total=>MARGIN*2 }
    pos = init_position(view)
    
    @sites.each do |s|
      button = new_button(view, s.name, pos)
      button.setState(OSX::NSOnState)
      button.setTarget(self)
      button.setAction(:site_clicked)
      @site_hash[button.object_id] = s.class.name.to_sym
      @site_btns << button

      # count site's categories
      cl = s.get_category_list
      cl.each do |c| 
        if @categories.has_key?(c) then
           @categories[c]+=1 
        else
           @categories[c]=1
           cat_order << c
        end
      end
      
      sclass_sym = s.class.name.to_sym
      @sclass_counter.has_key?(sclass_sym) ? @sclass_counter[sclass_sym]+=1 : @sclass_counter[sclass_sym]=1
    end
    
    new_line(pos)
    
    # categories
    @cat_buttons = []
    cat_order.each do |c|
      button = new_button(view, c, pos)
      @cat_buttons << button
    end

    new_line(pos)
    
    @tag_field = new_text(view, 'Tags', nil, pos)
    
    # other controls
    site_classes = []
    @sites.each do |s|
      unless site_classes.include?(s.class) then
        sclass_sym = s.class.name.to_sym
        @sclass_controls[sclass_sym] = []
        site_classes << s.class
        if s.class.get_fields then
          fields = s.class.get_fields
          fields.each do |fn, ft, dv|
            case ft
            when :bool
              control = new_button(view, fn, pos)
              # control.setState(OSX::NSOnState) if dv == true
            when :text
              control = new_text(view, make_word(fn.to_s), nil, pos)
            end
            @sclass_controls[sclass_sym] << control
            new_line(pos)
          end
        end
      end
    end
    
    # make parents taller if needed
    make_taller(view, pos[:total]) if view.frame.height < pos[:total] 
  end  
  
  def awakeFromNib
    OSX::NSNotificationCenter.defaultCenter.addObserver_selector_name_object(self, 'text_changed:', OSX::NSTextDidChangeNotification, nil)
    @blog_data_source = BlogDataSource.alloc.init
    @blog_data_source.root = ArticleSummary.get_summary
    @article_list.setDataSource(@blog_data_source)
    @article_list.setDelegate(self)
    decorate(@box.contentView)
    # p @content.respond_to?(:concludeDragOperation)
    # p @content.respondsToSelector('concludeDragOperation:')
    # @content.registerForDraggedTypes([OSX::NSStringPboardType])
    new_article
  end
  
  def text_changed(noti)
    @article_changed = true
  end
  
  def site_clicked(sender)
    add_num = (sender.state == OSX::NSOnState ? 1 : -1)

    # update category buttons
    site = find_site(sender.title.to_s)
    site.get_category_list.each do |c|
      btn = search_category_button(c)
      case (@categories[c] += add_num)
      when 0
        btn.setEnabled(false)
      when 1
        btn.setEnabled(true)
      end
    end
    
    # update class controls
    sclass_sym = @site_hash[sender.object_id]
    @sclass_counter[sclass_sym] += add_num
    case @sclass_counter[sclass_sym]
    when 0      # disable class related controls 
      set_site_controls_status(@sclass_controls[sclass_sym], false)
    when 1
      set_site_controls_status(@sclass_controls[sclass_sym], true)
    end
  end
  
  def view_article(sender)
    html = @sites[0].textilize(@content.string.to_s)
    # @webview.mainFrame.loadData_MIMEType_textEncodingName_baseURL(html, 'text/html', 'utf-8', '.')
    @webview.mainFrame.loadHTMLString_baseURL(html, nil)

    @preview.setTitle(@title.stringValue)
    @preview.display
    # @preview.makeKeyAndOrderFront       # this does not work well, use follwing 2 methods instead
    @preview.orderFrontRegardless
    @preview.makeKeyWindow
  end
  
  def post_article(sender)
    # clicked_site.each |s| post
    article = harvest
        
    clicked_names = get_selected_sites
    get_alert("No sites selected...").runModal if clicked_names.size == 0
    passed_sites = []
    marks = []
    @sites.each do |s|
      next unless clicked_names.include?(s.name)
      mark = s.validate_post(article)
      if mark == true then
        passed_sites << s
      else
        marks << "#{s.name} : #{mark}"
        # get_alert("Something's wrong...", :inform=>mark).runModal
      end
    end
    get_alert("Something's wrong...", :inform=>marks.join(' ')).runModal if marks.size > 0
    return if (clicked_names.size == 0) or (clicked_names.size != passed_sites.size)          # don't post if unqualified
    
    if @article_id.nil? then
      p 'creating new article'
      db_article = Article.create(:title=>article[:title], :content=>article[:content], :created_at=>Time.now,
                      :updated_at=>Time.now, :tags=>article[:tag], :custom=>article[:class])
    else
      db_article = Article.find(@article_id)
      db_article.update_attributes(:title=>article[:title], :content=>article[:content],
                      :updated_at=>Time.now, :tags=>article[:tag], :custom=>article[:class])
    end

    # media = []
    # URI.extract(article[:content]).each do |u|
    #   up = URI.parse(u)
    #   next if up.scheme != 'file'
    #   path = (up.path[-1] == 33) ? up.path[0...-1] : up.path        # URI module does not strip '!'
    #   m = Media.find_or_create(path)
    #   media << [up.scheme + '://' + path, m] if m
    # end
    media = media_lists(article[:content])
        
    passed_sites.each do |s|
      copy = Marshal::load(Marshal.dump(article))

      media.each do |localpath, m|
        if m.sites.include?(s) then
          mp = m.medialinks.find(:first, :conditions=>{:media_id=>m.id, :site_id=>s.id})
        else
          murl = s.post_media(m)
          mp = Medialink.create(:media_id=>m.id, :site_id=>s.id, :murl=>murl)
        end
        copy[:content].gsub!(localpath, mp.murl)
      end

      pub = db_article.publications.find(:first, :conditions=>{:article_id => db_article.id, :site_id => s.id})
      unless pub.nil? or pub.pub_id.nil? then    # repost
        p "site #{s.name} already published..."
        # p = s.publications.find(:first, :conditions=>{:article_id => @article_id, :site_id => s.id})
        copy[:pub_id] = pub.pub_id
        s.post_article(copy)
        pub.update_attributes(:published_at => Time.now)
      else                                    # new post
        p "not published at site #{s.name} before..."
        posted_article = s.post_article(copy)
        pub = Publication.new(:article_id => db_article.id, :site_id => s.id, :published_at => Time.now, :pub_id => posted_article[:pub_id])
        db_article.publications << pub
      end
    end
    
    update_categories(db_article)
    @article_id = db_article.id
    p "post article #{@article_id}\r\n"

    # remove from drafts list, move to recent list, refresh, open recent list and select 1st one
    dindex = @blog_data_source.root[0].index(@article_id)
    rindex = @blog_data_source.root[1][0].index(@article_id)

    @blog_data_source.root[0].delete_at(dindex) unless dindex.nil?
    if rindex.nil? then
      @blog_data_source.root[1][0].unshift(ArticleProxy.alloc.init.assign(db_article))
    else
      @blog_data_source.root[1][0].move_to_top(rindex)
    end
    @article_list.reloadItem_reloadChildren(nil, true)
    @article_list.expandItem(@blog_data_source.root[1])
    @article_list.expandItem(@blog_data_source.root[1][0])
    @article_list.selectRowIndexes_byExtendingSelection(OSX::NSIndexSet.alloc.initWithIndex(@blog_data_source.root[0].size+3), false)
    p "post article #{@article_id}\r\n"
  end
  
  def save_article(sender)
    # just save article
    article = harvest
    check = Article.validate_save(article[:title], article[:content])
    unless check.nil? then
      get_alert(check).runModal      
      return
    end
    
    if @article_id.nil? then
      db_article = Article.create(:title=>article[:title], :content=>article[:content], 
                      :created_at=>Time.now, :updated_at=>Time.now, :tags=>article[:tag], :custom=>article[:class])
      get_selected_sites.each { |s| db_article.sites << find_site(s) }
    else
      db_article = Article.update(@article_id, :title=>article[:title], :content=>article[:content], 
                      :updated_at=>Time.now, :tags=>article[:tag], :custom=>article[:class])
    end

    update_categories(db_article)
    
    # add to drat list
    if @article_id.nil? then
      @blog_data_source.root[0].unshift(ArticleProxy.alloc.init.assign(db_article))
      @article_list.reloadItem_reloadChildren(@blog_data_source.root[0], true)
      @article_list.expandItem(@blog_data_source.root[0])
      @article_list.selectRowIndexes_byExtendingSelection(OSX::NSIndexSet.alloc.initWithIndex(1), false)
    end
  end
  
  def new_article(sender=nil)
    # rest is handled at outlineViewSelectionDidChange
    if @article_list.selectedRow >= 0
      @article_list.deselectRow(@article_list.selectedRow)
    else
      @article_id = nil
      article = { :title=>'', :content=>'', :tags=>'' }
      seed(article)
      @main_win.makeFirstResponder(@title)
    end
  end
  
  def delete_draft(sender)
    sindex = @blog_data_source.root[0].index(@article_id)
    if (sindex and sindex >=0 and sindex <= @blog_data_source.root[0].size) then
      ret = get_alert('Really delete draft ?', :buttons=>['Cancel', 'Yes']).runModal
      if ret == OSX::NSAlertSecondButtonReturn then
        Article.delete(@blog_data_source.root[0][sindex].db_id)
        @blog_data_source.root[0].delete_at(sindex)
        @article_list.reloadItem_reloadChildren(@blog_data_source.root[0], true)
        @article_list.deselectRow(sindex)
      end
    end
  end
  
  def outlineView_shouldSelectItem(view, item)
    # called twice if returns false ?
    # p 'ShouldSelectItem:', view, item
    rvalue = (item.class == OutlineItem) ? false : true
    if rvalue and @article_changed then
      ret = get_alert('Proceed Without Saving?', :buttons=>['Yes', 'Cancel'], :inform=>'Changes will be lost.').runModal
      # alert.beginSheetModalForWindow_modalDelegate_didEndSelector_contextInfo(@main_win, self, "alertDidEnd:returnCode:contextInfo", "ADC")
      return false if ret == OSX::NSAlertSecondButtonReturn
    end
    return rvalue
  end
  
  def outlineViewSelectionDidChange(noti)
    proxy = @article_list.itemAtRow(@article_list.selectedRow)
    @article_id = proxy.nil? ? nil : proxy.db_id
    # p "#{@delete} : #{@article_list.selectedRow} vs #{@blog_data_source.root[0].size}"
    # this is not working yet... why?
    @delete.enabled =  (@article_list.selectedRow >=0 and @article_list.selectedRow <= @blog_data_source.root[0].size)  ? true : false
    if @article_id.nil? then 
      article = { :title=>'', :content=>'', :tags=>'' }
    else
      article = Article.find(@article_id)
    end
    seed(article)
    @article_changed = false
    @main_win.makeFirstResponder(@title) if @article_id.nil?
  end
  
  # def alertDidEnd_returnCode_contextInfo(alert, rcode, context)
  #  p rcode, context
  # end
  
  private
  
  def make_word(rvalue)
    rvalue.split('_').collect { |w| w.capitalize }.join(' ')
  end
  
  def refresh_article_list(index)
    lists = (index == 0) ? ArticleSummary.get_drafts : ArticleSummary.get_published
    @blog_data_source.root[index].update(lists)
    @article_list.reloadItem_reloadChildren(@blog_data_source.root[index], true)
  end
  
  def get_alert(message, params={})
  alert = OSX::NSAlert.alloc.init
  alert.setAlertStyle(OSX::NSWarningAlertStyle)
  alert.setMessageText(message)
  alert.setInformativeText(params[:inform]) if params.has_key?(:inform)
  if (params[:buttons].class == Array) and (params[:buttons].size < 4) then
    params[:buttons].each { |b| alert.addButtonWithTitle(b) }
  else
    alert.addButtonWithTitle('Yes')
  end
  alert
  end
    
  # returns editings as a dictionary
  def harvest
    result = {}
    result[:title] = @title.stringValue.to_s.strip
    result[:content] = @content.string.to_s.rstrip
    result[:tag] = @tag_field.stringValue.to_s.strip
    result[:categories] = get_selected_categories
    result[:class] = {}
    @sclass_counter.each_key { |cs| result[:class][cs] = harvest_controls(cs) }
    result
  end
  
  # set controls according to article dictionary
  def seed(article)
    @title.setStringValue(article[:title])
    @content.setString(article[:content])
    @tag_field.setStringValue(article[:tags])
    if @article_id.nil? then              # new article
      article_categories = []
      @site_btns.each do |btn| 
        btn.state = OSX::NSOnState
        btn.enabled = true
      end
      classes = {}
      site_symbols.each do |site_sym|
        classes[site_sym] = {}
        fields = eval("#{site_sym}.get_fields")
        fields.each_index do |i|
          fn, ft, dv = fields[i]
          classes[site_sym][fn] = case ft
            when :bool
              dv.nil? ? true : dv
            when :text
              dv.nil? ? '' : dv
          end
        end
      end
    else                                  # existing article
      saved_sites = article.sites.collect { |s| s.name }
      p saved_sites
      @site_btns.each do |btn|
        btn.state = saved_sites.include?(btn.title.to_s) ? OSX::NSOnState : OSX::NSOffState
        
        # enable button if not published but just saved
        publication = article.publications.find(:first, :conditions=>{:site_id=>find_site(btn.title.to_s)})
        btn.enabled = (publication.nil? or publication.pub_id.nil?) ? true : false
      end

      article_categories = article.get_category_list
      classes = article[:custom]
    end
    @cat_buttons.each do |b|
      b.state = article_categories.include?(b.title.to_s) ? OSX::NSOnState : OSX::NSOffState
    end
    classes.each_key do |cs|
      seed_controls(cs, classes[cs]) if classes[cs].size > 0 
    end
  end
  
  # find and return site object with 'name'
  def find_site(name)
    @sites.select { |s| s.name == name }[0]
  end
  
  # find and return category button with 'name'
  def search_category_button(name)
    @cat_buttons.select { |cb| cb.title.to_s == name }[0]
  end
  
  # 
  def set_site_controls_status(controls, state)
    controls.each { |c| c.setEnabled(state) }
  end
  
  def get_selected_sites
    clicked_names = []
    @site_btns.each do |btn|
      clicked_names << btn.title.to_s if btn.state == OSX::NSOnState
    end
    clicked_names
  end
  
  # check and return array of checked category names
  def get_selected_categories
    @cat_buttons.collect { |c| (c.state == OSX::NSOnState) ? c.title.to_s : nil}.compact
  end
  
  # update categories of article (on database) to currently checked category values
  def update_categories(db_article)
    p db_article.title, db_article.get_category_list
    new_list = get_selected_categories
    old_list = db_article.categories.collect { |c| c.name }
  
    removed = old_list - new_list
    db_article.categories.each { |c| db_article.categories.delete(c) if removed.include?(c.name) }
    (new_list - old_list).each { |c| db_article.categories << Category.new(:name=>c, :owner=>db_article) }
  end
  
  # check and return dictionary related to specific site class
  def harvest_controls(site_sym)
    result = {}
    fields = eval("#{site_sym}.get_fields")
    fields.each_index do |i|
      fn, ft, dv = fields[i]
      result[fn] = case ft
                  when :bool
                    @sclass_controls[site_sym][i].state == OSX::NSOnState
                  when :text
                    @sclass_controls[site_sym][i].stringValue.to_s
                  end
    end 
    result
  end
  
  def seed_controls(site_sym, values)
    fields = eval("#{site_sym}.get_fields")
    fields.each_index do |i|
      fn, ft, dv = fields[i]
      case ft
      when :bool
        @sclass_controls[site_sym][i].state = values[fn] ? OSX::NSOnState : OSX::NSOffState
      when :text
        @sclass_controls[site_sym][i].setStringValue(values[fn])
      end
    end     
  end
  
  def site_symbols
    @sclass_counter.keys
  end
  
  def media_lists(content)
    re = Regexp.new("(file://([^!\'\"\r\n]+))", Regexp::MULTILINE, 'u')
    list = []
    while re.match(content) do
      path = realpath($2)
      m = Media.find_or_create(path)
      list << [$1, m] if (path and m)
      content = $'
    end
    list
  end
  
  def realpath(d)
    return d if File.file?(d) 
    begin
      sp = d.rindex(/\s/)           # position of space character from back
      d = d[0...sp].rstrip          # strips off additional spaces
      return d if File.file?(d)
    end until sp.nil?
    nil
  end
end

class SubContent < OSX::NSTextView  
  # ns_overrides :performDragOperation_, :concludeDragOperation_
  # def performDragOperation(sender)
  #    p 'PerformDragOperation', sender
  #    super_performDragOperation(sender)
  # end
  # 
  def concludeDragOperation(sender)
     # p 'ConcludeDragOperation', sender
     super_concludeDragOperation(sender)
     range = selectedRanges[0].rangeValue
     source = string[range.location...(range.location+range.length)].to_s
     if Media.is_picture?(source) then
       insertText('!file://'+source+'!')
     elsif File.exist?(source) and File.ftype(source) == 'file' then
       insertText('file://'+source)
     end
     Media.find_or_create(source)
     setSelectedRange([0,0])
   end
end
